#!/usr/bin/env python

'''
  MUDServ - A Multi-User Dungeon Server
  Main server Source Code
  
  Copyright (c) 2009, Ecl3ctic Software
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:
  
  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  * Neither the name of Ecl3ctic Software nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
'''

import socket
import sys
import threading
import mutex
import os, os.path
import logging
from time import sleep
from ConfigParser import *
import sha
import traceback
import datetime
import platform
import signal
from clientlib import *
from webserver import *
from worldmap import *
from model import *
from elixir import *

__version__ = '04-12-09'
__author__ = 'Ian Wagner and Michael Chest'
__credits__ = ['Ian Wagner', 'Michael Chest', 'Brian McBrayer', 'Dan Alexander',
               'Zak Richards', 'Reuben Deleon']
__license__ = 'BSD'
__status__ = 'Development'
__copyright__ = 'Copyright 2009 Ecl3ctic Software'

# Check if we are a daemon process
daemon = False
if len(sys.argv) > 2 or platform.system() == 'Windows':
  if platform.system() == 'Windows':
    print 'Usage: server.py'
  else:
    print 'Usage: server.py [-d]'
  print
  print 'Options:'
  print '-d\tRun as a daemon process'
  sys.exit(1)
elif '-d' in sys.argv and platform.system() != 'Windows':
  try:
    pid = os.fork()
  except OSError, e:
    raise Exception, "%s [%d]" % (e.strerror, e.errno)
  
  if pid == 0:
    print 'Entering daemon mode...'
    daemon = True
  else:
    os._exit(0)

# Setup logging
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s',
                    filename='mudserv.log')
console = logging.StreamHandler()
console.setLevel(logging.ERROR)
formatter = logging.Formatter('%(asctime)-12s: %(levelname)-8s %(message)s')
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)

# Server configuration
config = SafeConfigParser()
config.readfp(open('server.cfg'))

port = config.getint('server', 'server_port')
web_port = config.getint('server', 'web_port')
timeout = config.getfloat('server', 'socket_timeout')
socket.setdefaulttimeout(timeout)

# Keep a list of threads
threads = []
lock = mutex.mutex()

# Publicly executable commands (not requiring login)
publicCommands = ['login', 'motd', 'version', 'getasciiboard', 'getboarddimensions', 'getnearbyplayers', 'who']

# GM-only commands
gmCommands = ['kick', 'ban', 'wall']

# List of hidden server-side users (do not include in output of who)
hiddenUsers = ['admin']

# List of forbidden names
forbiddenNames = ['admin', 'gm', 'administrator', 'game master', 'gm message']

# List of methods in the server class that should NOT be executed
noExecute = ['_Thread__args', '_Thread__block',
             '_Thread__bootstrap', '_Thread__bootstrap_inner',
             '_Thread__daemonic', '_Thread__delete', '_Thread__exc_info',
             '_Thread__initialized', '_Thread__kwargs', '_Thread__name',
             '_Thread__started', '_Thread__stderr', '_Thread__stop',
             '_Thread__stopped', '_Thread__target', '_Verbose__verbose',
             '__class__', '__delattr__', '__dict__', '__doc__',
             '__getattribute__', '__hash__', '__init__', '__module__',
             '__new__', '__reduce__', '__reduce_ex__', '__repr__',
             '__setattr__', '__str__', '__weakref__', '_note', '_set_daemon',
             'getName', 'ip', 'isAlive', 'isDaemon', 'isGM', 'join', 'killed',
             'messages', 'run', 'setDaemon', 'setName', 'sock', 'start',
             'username', 'board', 'x', 'y', 'getTile']

# List of commands that do not want arguments split, but take a signle string
noSplitArgs = ['setname', 'wall', 'move']

worldMap = WorldMap()

class Server(threading.Thread):
  '''
    Basic threaded server class
    
    Each server thread will handle one client connection.
  '''
  def __init__(self, sock):
    threading.Thread.__init__(self)
    self.sock = sock
    self.username = ''
    self.isGM = False
    self.ip = ''
    self.boardID = None
    self.board = None
    self.asciiBoard = ''
    self.nextUpdate = datetime.datetime.now()
    self.me = None
    self.x = None
    self.y = None
    
    fp = open('motd.txt', 'rU')
    motd = fp.readlines()
    fp.close()
    
    self.messages = [''.join(motd)]   # Populate messages with motd
    self.killed = False
    
  def kill(self):
    self.killed = True
    
  def run(self):
    self.ip = self.sock.getpeername()[0]
    
    try:
      while self.username == '':
        result = self.recvCommand()
        if result[0] in publicCommands:
          eval('self.' + result[0])(result[1])
        elif result[0] == 'quit':
          logging.info('Client connecting from ' + self.ip + ' disconnected normally')
          self.sock.close()
          return
        else:
          self.sock.send('/-1/')
      
      # Log the user info
      logging.info('Client connecting from ' + self.ip + ' logged in with username ' + self.username)
      # Main loop
      while not self.killed:
        result = self.recvCommand()
        if result[0] == 'quit':
          break
        elif result[0] not in noExecute and (result[0] not in gmCommands or self.isGM) and hasattr(self, result[0]):
          try:
            eval('self.' + result[0])(result[1])
          except:
            self.sock.send('/-2/')
            logging.error(traceback.format_exc())
        else:
          self.sock.send('/-1 Command not found or insufficient permissions/')
      
      logging.info('Client connecting from ' + self.ip + ' disconnected normally')
      self.sock.close()
    except:
      logging.info('Client connecting from ' + self.ip + '\'s connection was abnormally terminated')
      logging.error(traceback.format_exc())
      self.sock.close()
      
    logMeOut = User.query.filter_by(username=self.me.username).first() #reload user for greatest effectiveness (basically guarantees a session is started)
    logMeOut.online = False
    session.commit()
    
  def recvCommand(self):
    '''
      Waits to receive a command from the connected user
      
      If an error occurs, returns (-1), else returns a tuple containing the
      name of the command, and a list of arguments
    '''
    c = ''
    cmd = ''
    arg = ''
    args = []
    commandFinished = False
    commandStarted = False
    cmdReceived = False
    
    while not self.killed:
      try:
        c = self.sock.recv(1)
        if '/' in c:
            if commandStarted:
                commandFinished = True
                commandStarted = False
            else:
                commandStarted = True
        
        if ' ' in c and not cmdReceived:
          cmdReceived = True
          c = c.lstrip()
            
        if cmdReceived:
          arg += c.strip('/')
        else:
          cmd += c.strip('/')
        
        if commandFinished:
            break
      except:
        logging.error('Error communicating with client from ' + self.ip)
        return ('quit', '')
    
    cmd = cmd.lower()
    args = arg
    if cmd not in noSplitArgs:
      args = args.split(' ')
      
    logging.debug('Client from ' + self.ip + ' issued command: ' + cmd + ' with args ' + str(args))
    return (cmd, args)
  
  def version(self, args):
    '''
      Returns the server version
    '''
    self.sock.send('/0 ' + __version__ + '/')
  
  def login(self, args):
    '''
      Logs in the connected user
    '''
    if self.username:
      self.sock.send('/1/')
    
    resetPlayer = False
    
    #
    # TODO: Check if user is banned!
    #
    
    if len(args) > 0:
      if args[0].strip() == 'admin' and self.ip != '127.0.0.1':
        self.sock.send('/1/')
      else:
        result = User.query.filter_by(username=unicode(args[0].strip())).first()
        
        password = sha.new(args[1].strip()).hexdigest()
        
        if not result or result.password == password:
          self.username = args[0].strip()
          logging.info('Client from ' + self.ip + ' has logged in with username ' +
                     self.username)
          
          if self.username == 'admin' or (result and result.gm):
            self.isGM = True
            logging.info(self.username + ' logged in as a GM')
          
        else:
          logging.info('Client from ' + self.ip + ' failed to log in with username ' +
                        self.username)
          self.sock.send('/1/')
          
        if not result:
          logging.info('Client from ' + self.ip + ' has created an account with username ' +
                        self.username)
          
          if self.username == 'admin':
            gm = True
            self.isGM = True
          else:
            gm = False
          
          result = User(username=unicode(self.username), password=unicode(password), gm=gm)
        if self.username == 'admin':
          self.sock.settimeout(None)
        else:
          self.sock.settimeout(timeout)
          result.online = True

        session.commit()

        if result.board == -1 or result.board == None:
          result.board = worldMap.fetchBoard(0, 0, 0).id
          resetPlayer = True
        board = Board.query.filter_by(id=result.board).first()
        self.boardID = board.id
        self.board = simplejson.loads(board.data)
        
        self.asciiBoard = ''
        for y in range(self.board.get('height')):
          for x in range(self.board.get('width')):
            self.asciiBoard += self.getTile(x, y)
          self.asciiBoard += '\n'
          
        self.x = result.pos_x
        self.y = result.pos_y
        
        if (not self.x and not self.y) or resetPlayer:
          self.x = self.board.get('spawn', {}).get('main', {}).get('x')
          self.y = self.board.get('spawn', {}).get('main', {}).get('y')
          
          if not self.x or not self.y:
            # No spawn point!
            self.sock.send('/1/')
            return
          
          result.pos_x = self.x
          result.pos_y = self.y
        
        self.me = result
        
        self.sock.send('/0/')
    else:
      self.sock.send('/1/')
      
  def motd(self, args):
    '''
      Message of the day
    '''
    fp = open('motd.txt', 'rU')
    motd = fp.readlines()
    fp.close()
    self.sock.send('/0 ' + ''.join(motd) + '/')
    
  def getinfo(self, args):
    '''
      Get information about a player
    '''
    result = User.query.filter_by(username=unicode(args[0])).first()
    if not result:
      self.sock.send('/1 Unknown user/')
    elif args[1] == 'pos':
      self.sock.send('/0 %d %d/' % (result.pos_x, result.pos_y))
    elif args[1] == 'password' or not hasattr(result, args[1]):
      self.sock.send('/2 Invalid parameter/')
    else:
      self.sock.send('/0 ' + str(eval('result.' + args[1])) + '/')
    
  def who(self, args):
    '''
      Get information about who is online
    '''
    users = []
    for thread in threading.enumerate():
      if hasattr(thread, 'username') and thread.username not in hiddenUsers:
        gmText = ''
        if thread.isGM:
          gmText = '[GM]'
        users.append(thread.username + gmText)
        
    if users == []:
      users = ['No Users Online']
        
    self.sock.send('/0 ' + '\n'.join(users) + '/')
    
  def setname(self, name):
    '''
      Sets the full name of the logged in user
    '''
    if name.lower() in forbiddenNames or User.query.filter_by(fullname=unicode(name)).first() != None:
      self.sock.send('/1/')
      return
    
    user = User.query.filter_by(username=unicode(self.username)).first()
    user.fullname = unicode(name)
    
    self.sock.send('/0/')
    
  def getboarddimensions(self, args):
    self.sock.send('/0 %d %d/' % (worldMap.boardWidth, worldMap.boardHeight))
  
  def getasciiboard(self, args):
    '''
      Returns and ascii representation of the board at args[0],args[1],args[2]
      if args are present, otherwise return the current board the player is on
    '''
    if len(args) == 3:
      asciiBoard = ''
      board = simplejson.loads(worldMap.fetchBoard(args[0], args[1], args[2]).data)
      
      for y in range(board.get('height')):
        for x in range(board.get('width')):
          asciiBoard += board.get(str(y), {}).get(str(x), {}).get('ascii', ' ')
        asciiBoard += '\n'
      
      self.sock.send('/0 ' + asciiBoard + '/')
    else:
      self.sock.send('/0 ' + self.asciiBoard + '/')
    
  def getnearbyplayers(self, args):
    '''
      With no arguments, this will return the players on the current player's
      board. Otherwise, it will return the players from the board whose
      coordinates are given in args[0], args[1], and args[2]
    '''
    if len(args) == 3:
      players = User.query.filter_by(board=worldMap.fetchBoard(args[0], args[1], args[2], False).id).filter_by(online=True).all()
    else:
      players = User.query.filter_by(board=self.boardID).filter_by(online=True).all()
    
    self.sock.send('/0 ' + simplejson.dumps([{'x': player.pos_x, 'y': player.pos_y, 'username': player.username} for player in players]) + '/')
    
  def getTile(self, x, y):
    '''
      Helper function to get a tile from the current board at position (x, y)
      because honestly that's one confusing line of JSON :)
    '''
    return self.board.get(str(y), {}).get(str(x), {}).get('ascii', ' ')
    
  def move(self, dir):
    '''
      Attempts to move in the direction given by dir
      
      Valid directions:
        n- North
        s- South
        e- East
        w- West
        u- Up
        d- Down
    '''
    changed = False
    boardChanged = False
    
    self.me = User.query.filter_by(username=unicode(self.username)).first()
    
    if dir == 'n':
      char = self.getTile(self.x, self.y - 1)
      if char not in hardTiles:
        if self.y == 0:
          # Check if a board exists north of us
          board = Board.query.filter_by(id=self.me.board).first()
          nextBoard = worldMap.fetchBoard(board.x, board.y + 1, board.z)
          
          self.me.board = nextBoard.id
          self.boardID = nextBoard.id
          self.board = simplejson.loads(nextBoard.data)
          self.y = self.board.get('spawn').get('south').get('y') - 1
          session.commit()
          boardChanged = True
        else:
          self.y -= 1
        changed = True
    elif dir == 's':
      char = self.getTile(self.x, self.y + 1)
      if char not in hardTiles:
        if self.y == self.board.get('height') - 1:
          # Check if a board exists south of us
          board = Board.query.filter_by(id=self.me.board).first()
          nextBoard = worldMap.fetchBoard(board.x, board.y - 1, board.z)
          
          self.me.board = nextBoard.id
          self.boardID = nextBoard.id
          self.board = simplejson.loads(nextBoard.data)
          self.y = self.board.get('spawn').get('north').get('y') + 1
          session.commit()
          boardChanged = True
        else:
          self.y += 1
        changed = True
    elif dir == 'e':
      char = self.getTile(self.x + 1, self.y)
      if char not in hardTiles:
        if self.x == self.board.get('width') - 2:
          # Check if a board exists east of us
          board = Board.query.filter_by(id=self.me.board).first()
          nextBoard = worldMap.fetchBoard(board.x + 1, board.y, board.z)
          
          self.me.board = nextBoard.id
          self.boardID = nextBoard.id
          self.board = simplejson.loads(nextBoard.data)
          self.x = self.board.get('spawn').get('west').get('x') + 1
          session.commit()
          boardChanged = True
        else:
          self.x += 1
        changed = True
    elif dir == 'w':
      char = self.getTile(self.x - 1, self.y)
      if char not in hardTiles:
        if self.x == 0:
          # Check if a board exists west of us
          board = Board.query.filter_by(id=self.me.board).first()
          nextBoard = worldMap.fetchBoard(board.x - 1, board.y, board.z)
          
          self.me.board = nextBoard.id
          self.boardID = nextBoard.id
          self.board = simplejson.loads(nextBoard.data)
          self.x = self.board.get('spawn').get('east').get('x') - 1
          session.commit()
          boardChanged = True
        else:
          self.x -= 1
        changed = True
    elif dir == 'u':
      pass
    elif dir == 'd':
      pass
      
    # Update the database
    if changed:
      self.me.pos_x = self.x
      self.me.pos_y = self.y
      
    if boardChanged:
      self.nextUpdate = datetime.datetime.now()
      self.asciiBoard = ''
      for y in range(self.board.get('height')):
        for x in range(self.board.get('width')):
          self.asciiBoard += self.getTile(x, y)
        self.asciiBoard += '\n'
    
    msg = ""
    char = self.getTile(self.x - 1, self.y) #w
    if char not in hardTiles:
      msg = msg + " w"
    char = self.getTile(self.x + 1, self.y) #e
    if char not in hardTiles:
      msg = msg + " e"
    char = self.getTile(self.x, self.y + 1) #s
    if char not in hardTiles:
      msg = msg + " s"
    char = self.getTile(self.x, self.y - 1) #n
    if char not in hardTiles:
      msg = msg + " n"

    if changed or boardChanged:
      self.sock.send('/0' + msg + '/')
      session.commit()
    else:
      self.sock.send('/1' + msg + '/')
      
  def tell(self, args):
    '''
      Puts message (args[1]) in user's (args[0]) message queue
    '''
    code = 1
    
    user = args[0]
    sender = User.query.filter_by(username=unicode(self.username)).first().fullname
    if sender == None:
      sender = self.username
    message = '*privmsg* ' + sender + ': ' + ' '.join(args[1:])
    
    for thread in threading.enumerate():
      if hasattr(thread, 'username') and thread.username == user:
        thread.messages.append(message)
        code = 0
    
    self.sock.send('/%d/' % (code))
    
  def getmessage(self, args):
    '''
      Fetches the next message from the user's message queue or returns status
      code 1 if all messages have been received
    '''
    if len(self.messages) == 0:
      self.sock.send('/1/')
    else:
      self.sock.send('/0 %s/' % (self.messages.pop(0)))
      
  def wall(self, args):
    '''
      Sends a GM message to all connected players
    '''
    message = '*GM Message*: ' + args
    
    for thread in threading.enumerate():
      if hasattr(thread, 'username'):
        thread.messages.append(message)
        
    self.sock.send('/0/')
    
class DatabaseUpdater(threading.Thread):
  '''
    Database updater class
    
    This is launched as a thread by the server and syncs database writes every
    5 seconds.
  '''
  def __init__(self):
    threading.Thread.__init__(self)
    self.killed = False
    
  def kill(self):
    self.killed = True
    
  def run(self):
    while not self.killed:
      session.commit()
      sleep(5.0)
    print 'Database updater has terminated'
    
class WorldUpdater(threading.Thread):
  '''
    World updater class
    
    One world updater is created for the server and periodically builds on to the
    existing game world to the north, south, east, and west of all of the players
    to minimize the time spent waiting for maps to generate.
  '''
  def __init__(self):
    threading.Thread.__init__(self)
    self.killed = False
    
  def kill(self):
    self.killed = True
    
  def run(self):
    while not self.killed:
      users = [thread.me for thread in threads if hasattr(thread, 'me') and thread.me != None]
      
      for user in users:
        # Generate boards north, south, east, and west of all players
        board = Board.query.filter_by(id=user.board).first()
        if board:
          worldMap.generateBoard(board.x - 1, board.y, board.z,
                                 worldMap.boardHeight, worldMap.boardWidth)
          worldMap.generateBoard(board.x + 1, board.y, board.z,
                                 worldMap.boardHeight, worldMap.boardWidth)
          worldMap.generateBoard(board.x, board.y - 1, board.z,
                                 worldMap.boardHeight, worldMap.boardWidth)
          worldMap.generateBoard(board.x, board.y + 1, board.z,
                                 worldMap.boardHeight, worldMap.boardWidth)
      sleep(5.0)
      
    print 'World updater has terminated'
    
class ConnectionSpawner(threading.Thread):
  def __init__(self, port):
    threading.Thread.__init__(self)
    self.port = port
    self.killed = False
    self.listener = None
    
  def kill(self):
    self.killed = True
    self.listener.close()
    
  def run(self):
    try:
      self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      self.listener.settimeout(5)
    
      self.listener.bind(('', self.port))
      self.listener.listen(10)
    except:
      print "Address in use. Exiting."
      return
    
    welcome = 'MUDServ version ' + __version__ + ': Startup complete. Listening on port ' + str(self.port)
    logging.info(welcome)
    print welcome
    
    while not self.killed:
      try:
        (client, ap) = self.listener.accept()
        logging.info('Client connected from ' + ap[0])
        s = Server(client)
        while not lock.testandset():
          sleep(0.1)
        threads.append(s)
        lock.unlock()
        s.start()
      except socket.timeout:
        pass
      except:
        killed = True
    
    self.listener.close()
    print 'Listener has terminated'
  
for s in threads:
  try:
    s.kill()
  except:
    pass
  s.join()

# Spawn the listener
listener = ConnectionSpawner(port)

while not lock.testandset():
  sleep(0.1)
threads.append(listener)
lock.unlock()

listener.start()

# Spawn the web server
webServ = WebServ(web_port, port)

while not lock.testandset():
  sleep(0.1)
threads.append(webServ)
lock.unlock()

webServ.start()

# Spawn the db updater
dbUpdater = DatabaseUpdater()

while not lock.testandset():
  sleep(0.1)
threads.append(dbUpdater)
lock.unlock()

dbUpdater.start()

# Spawn the world updater
worldUpdater = WorldUpdater()

while not lock.testandset():
  sleep(0.1)
threads.append(worldUpdater)
lock.unlock()

worldUpdater.start()

sleep(0.5)

if not listener.isAlive() or not webServ.isAlive():
  print "Threads could not start.  Exiting."
  sys.exit(0);

#
# Setup the admin console
#
if not daemon:
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  sock.connect(('localhost', port))
  executeCommand(sock, 'login', ['admin', 'foo'])

quit = False
sleep(0.42)

# Define the signal handler
def sigHandler(signum, frame):
  if not daemon:
    executeCommand(sock, 'quit')
    sock.close()
  
  quit = True
  
  for user in User.query.all():
    user.online = False
    
  session.commit()
  
  if threading.activeCount() > 1:
    message = 'Waiting for ' + str(threading.activeCount() - 1) + ' threads to terminate...'
    logging.info(message)
    print message
  
  for s in threads:
    try:
      s.kill()
    except:
      pass
    s.join()
  
  sys.exit(0)

# Install signal handler
#signal.signal(signal.SIGKILL, sigHandler)
signal.signal(signal.SIGHUP, sigHandler)
signal.signal(signal.SIGABRT, sigHandler)
signal.signal(signal.SIGINT, sigHandler)

while not quit:
  try:
    if not daemon:
      sys.stdout.write('MUDServ> ')
      sys.stdout.flush()
      cmd = sys.stdin.readline().strip()
      
      if len(cmd) > 0 and cmd != 'quit':
        command = cmd.split(' ')[0]
        
        args = []
        if len(cmd.split(' ')) > 1:
          args = cmd.split(' ')[1:]
        
        result = executeCommand(sock, command, args)
        
        print 'Status code: ' + str(result[0])
        print result[1]
    else:
      sleep(1.0)
  except KeyboardInterrupt:
    if not daemon:
      print '\n\nServer shutdown initiated...'
      quit = True
    
quit(None, None)